using System.Collections.Generic;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.batchinsert
{


	using GraphDatabaseService = org.neo4j.graphdb.GraphDatabaseService;
	using NotFoundException = org.neo4j.graphdb.NotFoundException;
	using RelationshipType = org.neo4j.graphdb.RelationshipType;
	using EmbeddedGraphDatabase = org.neo4j.kernel.EmbeddedGraphDatabase;
	using DynamicRecord = org.neo4j.kernel.impl.nioneo.store.DynamicRecord;
	using InvalidRecordException = org.neo4j.kernel.impl.nioneo.store.InvalidRecordException;
	using NeoStore = org.neo4j.kernel.impl.nioneo.store.NeoStore;
	using NodeRecord = org.neo4j.kernel.impl.nioneo.store.NodeRecord;
	using NodeStore = org.neo4j.kernel.impl.nioneo.store.NodeStore;
	using PropertyIndexData = org.neo4j.kernel.impl.nioneo.store.PropertyIndexData;
	using PropertyIndexRecord = org.neo4j.kernel.impl.nioneo.store.PropertyIndexRecord;
	using PropertyIndexStore = org.neo4j.kernel.impl.nioneo.store.PropertyIndexStore;
	using PropertyRecord = org.neo4j.kernel.impl.nioneo.store.PropertyRecord;
	using PropertyStore = org.neo4j.kernel.impl.nioneo.store.PropertyStore;
	using Record = org.neo4j.kernel.impl.nioneo.store.Record;
	using RelationshipRecord = org.neo4j.kernel.impl.nioneo.store.RelationshipRecord;
	using RelationshipStore = org.neo4j.kernel.impl.nioneo.store.RelationshipStore;
	using RelationshipTypeData = org.neo4j.kernel.impl.nioneo.store.RelationshipTypeData;
	using RelationshipTypeRecord = org.neo4j.kernel.impl.nioneo.store.RelationshipTypeRecord;
	using RelationshipTypeStore = org.neo4j.kernel.impl.nioneo.store.RelationshipTypeStore;
	using UnderlyingStorageException = org.neo4j.kernel.impl.nioneo.store.UnderlyingStorageException;
	using FileUtils = org.neo4j.kernel.impl.util.FileUtils;

	public class BatchInserterImpl : BatchInserter
	{
		private readonly NeoStore neoStore;
		private readonly string storeDir;

		private readonly PropertyIndexHolder indexHolder;
		private readonly RelationshipTypeHolder typeHolder;

		private readonly BatchGraphDatabaseImpl graphDbService;

		public BatchInserterImpl(string storeDir) : this(storeDir, Collections.EMPTY_MAP)
		{
		}

		public BatchInserterImpl(string storeDir, Map<string, string> stringParams)
		{
			Map<object, object> @params = getDefaultParams();
			@params.put("use_memory_mapped_buffers", "false");
			foreach (Map.Entry<string, string> entry in stringParams.entrySet())
			{
				@params.put(entry.Key, entry.Value);
			}
			this.storeDir = storeDir;
			string store = fixPath(storeDir);
			@params.put("neo_store", store);

		// TODO: check if clean shutdown

			neoStore = new NeoStore(@params);
			neoStore.makeStoreOk();
			PropertyIndexData[] indexes = getPropertyIndexStore().getPropertyIndexes(10000);
			indexHolder = new PropertyIndexHolder(indexes);
			RelationshipTypeData[] types = getRelationshipTypeStore().getRelationshipTypes();
			typeHolder = new RelationshipTypeHolder(types);
			graphDbService = new BatchGraphDatabaseImpl(this);
		}

		public virtual long createNode(Map<string, object> properties)
		{
			int nodeId = getNodeStore().nextId();
			NodeRecord nodeRecord = new NodeRecord(nodeId);
			nodeRecord.setInUse(true);
			nodeRecord.setCreated();
			nodeRecord.setNextProp(createPropertyChain(properties));
			getNodeStore().updateRecord(nodeRecord);
			return (nodeId & 0xFFFFFFFFL);
		}

		public virtual void createNode(long id, Map<string, object> properties)
		{
			if (id < 0 || id > 0xFFFFFFFFL)
			{
				throw new IllegalArgumentException("id=" + id);
			}
			int nodeId = (int)(id & 0xFFFFFFFF);
			NodeStore nodeStore = neoStore.getNodeStore();
			if (neoStore.getNodeStore().loadLightNode(nodeId))
			{
				throw new IllegalArgumentException("id=" + id + " already in use");
			}
			long highId = nodeStore.getHighId();
			if (highId <= id)
			{
				nodeStore.setHighId(nodeId + 1);
			}
			NodeRecord nodeRecord = new NodeRecord(nodeId);
			nodeRecord.setInUse(true);
			nodeRecord.setCreated();
			nodeRecord.setNextProp(createPropertyChain(properties));
			getNodeStore().updateRecord(nodeRecord);
		}

		public virtual long createRelationship(long node1, long node2, RelationshipType type, Map<string, object> properties)
		{
			if (node1 == node2)
			{
				throw new IllegalArgumentException("Start node[" + node1 + "] equals end node[" + node2 + "]");
			}
			int firstNodeId = (int)(node1 & 0xFFFFFFFF);
			int secondNodeId = (int)(node2 & 0xFFFFFFFF);
			NodeRecord firstNode = getNodeRecord(node1);
			NodeRecord secondNode = getNodeRecord(node2);
			int typeId = typeHolder.getTypeId(type.name());
			if (typeId == -1)
			{
				typeId = createNewRelationshipType(type.name());
			}
			int id = getRelationshipStore().nextId();
			RelationshipRecord record = new RelationshipRecord(id, firstNodeId, secondNodeId, typeId);
			record.setInUse(true);
			record.setCreated();
			connectRelationship(firstNode, secondNode, record);
			getNodeStore().updateRecord(firstNode);
			getNodeStore().updateRecord(secondNode);
			record.setNextProp(createPropertyChain(properties));
			getRelationshipStore().updateRecord(record);
			return id & 0xFFFFFFFFL;
		}

		private void connectRelationship(NodeRecord firstNode, NodeRecord secondNode, RelationshipRecord rel)
		{
			rel.setFirstNextRel(firstNode.getNextRel());
			rel.setSecondNextRel(secondNode.getNextRel());
			if (firstNode.getNextRel() != Record.NO_NEXT_RELATIONSHIP.intValue())
			{
				RelationshipRecord nextRel = getRelationshipStore().getRecord(firstNode.getNextRel());
				if (nextRel.getFirstNode() == firstNode.getId())
				{
					nextRel.setFirstPrevRel(rel.getId());
				}
				else if (nextRel.getSecondNode() == firstNode.getId())
				{
					nextRel.setSecondPrevRel(rel.getId());
				}
				else
				{
					throw new InvalidRecordException(firstNode + " dont match " + nextRel);
				}
				getRelationshipStore().updateRecord(nextRel);
			}
			if (secondNode.getNextRel() != Record.NO_NEXT_RELATIONSHIP.intValue())
			{
				RelationshipRecord nextRel = getRelationshipStore().getRecord(secondNode.getNextRel());
				if (nextRel.getFirstNode() == secondNode.getId())
				{
					nextRel.setFirstPrevRel(rel.getId());
				}
				else if (nextRel.getSecondNode() == secondNode.getId())
				{
					nextRel.setSecondPrevRel(rel.getId());
				}
				else
				{
					throw new InvalidRecordException(secondNode + " dont match " + nextRel);
				}
				getRelationshipStore().updateRecord(nextRel);
			}
			firstNode.setNextRel(rel.getId());
			secondNode.setNextRel(rel.getId());
		}

		public virtual void setNodeProperties(long node, Map<string, object> properties)
		{
			NodeRecord record = getNodeRecord(node);
			if (record.getNextProp() != Record.NO_NEXT_PROPERTY.intValue())
			{
				deletePropertyChain(record.getNextProp());
			}
			record.setNextProp(createPropertyChain(properties));
			getNodeStore().updateRecord(record);
		}

		public virtual void setRelationshipProperties(long rel, Map<string, object> properties)
		{
			RelationshipRecord record = getRelationshipRecord(rel);
			if (record.getNextProp() != Record.NO_NEXT_PROPERTY.intValue())
			{
				deletePropertyChain(record.getNextProp());
			}
			record.setNextProp(createPropertyChain(properties));
			getRelationshipStore().updateRecord(record);
		}

		public virtual bool nodeExists(long nodeId)
		{
			int id = (int)(nodeId & 0xFFFFFFFF);
			return neoStore.getNodeStore().loadLightNode(id);
		}

		public virtual Map<string, object> getNodeProperties(long nodeId)
		{
			NodeRecord record = getNodeRecord(nodeId);
			if (record.getNextProp() != Record.NO_NEXT_PROPERTY.intValue())
			{
				return getPropertyChain(record.getNextProp());
			}
			return Collections.EMPTY_MAP;
		}

		public virtual IEnumerable <long> getRelationshipIds(long nodeId)
		{
			NodeRecord nodeRecord = getNodeRecord(nodeId);
			int nextRel = nodeRecord.getNextRel();
			List<long> ids = new List<long>();
			while (nextRel != Record.NO_NEXT_RELATIONSHIP.intValue())
			{
				RelationshipRecord relRecord = getRelationshipRecord(nextRel);
				ids.Add(relRecord.getId() & 0xFFFFFFFFL);
				int firstNode = relRecord.getFirstNode();
				int secondNode = relRecord.getSecondNode();
				if (firstNode == nodeId)
				{
					nextRel = relRecord.getFirstNextRel();
				}
				else if (secondNode == nodeId)
				{
					nextRel = relRecord.getSecondNextRel();
				}
				else
				{
					throw new InvalidRecordException("Node[" + nodeId + "] not part of firstNode[" + firstNode + "] or secondNode[" + secondNode + "]");
				}
			}
			return ids;
		}

		public virtual IEnumerable <SimpleRelationship> getRelationships(long nodeId)
		{
			NodeRecord nodeRecord = getNodeRecord(nodeId);
			int nextRel = nodeRecord.getNextRel();
			List<SimpleRelationship> rels = new List<SimpleRelationship>();
			while (nextRel != Record.NO_NEXT_RELATIONSHIP.intValue())
			{
				RelationshipRecord relRecord = getRelationshipRecord(nextRel);
				RelationshipType type = new RelationshipTypeImpl(typeHolder.getName(relRecord.getType()));
				rels.Add(new SimpleRelationship(relRecord.getId(), relRecord.getFirstNode(), relRecord.getSecondNode(), type));
				int firstNode = relRecord.getFirstNode();
				int secondNode = relRecord.getSecondNode();
				if (firstNode == nodeId)
				{
					nextRel = relRecord.getFirstNextRel();
				}
				else if (secondNode == nodeId)
				{
					nextRel = relRecord.getSecondNextRel();
				}
				else
				{
					throw new InvalidRecordException("Node[" + nodeId + "] not part of firstNode[" + firstNode + "] or secondNode[" + secondNode + "]");
				}
			}
			return rels;
		}

		public virtual SimpleRelationship getRelationshipById(long relId)
		{
			RelationshipRecord record = getRelationshipRecord(relId);
			RelationshipType type = new RelationshipTypeImpl(typeHolder.getName(record.getType()));
			return new SimpleRelationship(record.getId(), record.getFirstNode(), record.getSecondNode(), type);
		}

		public virtual Map<string, object> getRelationshipProperties(long relId)
		{
			RelationshipRecord record = getRelationshipRecord(relId);
			if (record.getNextProp() != Record.NO_NEXT_PROPERTY.intValue())
			{
				return getPropertyChain(record.getNextProp());
			}
			return Collections.EMPTY_MAP;
		}

		public virtual void shutdown()
		{
			graphDbService.clearCaches();
			neoStore.Close();
		}

		private Map<object, object> getDefaultParams()
		{
			Map<object, object> @params = new Dictionary<object, object>();
			@params.put("neostore.nodestore.db.mapped_memory", "20M");
			@params.put("neostore.propertystore.db.mapped_memory", "90M");
			@params.put("neostore.propertystore.db.index.mapped_memory", "1M");
			@params.put("neostore.propertystore.db.index.keys.mapped_memory", "1M");
			@params.put("neostore.propertystore.db.strings.mapped_memory", "130M");
			@params.put("neostore.propertystore.db.arrays.mapped_memory", "130M");
			@params.put("neostore.relationshipstore.db.mapped_memory", "50M");
			return @params;
		}

		public override string ToString()
		{
			return "EmbeddedBatchInserted[" + storeDir + "]";
		}

		private class RelationshipTypeImpl : RelationshipType
		{
			private readonly string name;

			internal RelationshipTypeImpl(string name)
			{
				this.name = name;
			}

			public virtual string name()
			{
				return name;
			}
		}

		private int createPropertyChain(Map<string, object> properties)
		{
			if (properties == null)
			{
				return Record.NO_NEXT_PROPERTY.intValue();
			}
			PropertyStore propStore = getPropertyStore();
			List<PropertyRecord> propRecords = new List<PropertyRecord>();
			PropertyRecord prevRecord = null;
			foreach (KeyValuePair<string, object> entry in properties.entrySet())
			{
				int keyId = indexHolder.getKeyId(entry.Key);
				if (keyId == -1)
				{
					keyId = createNewPropertyIndex(entry.Key);
				}
				int propertyId = propStore.nextId();
				PropertyRecord propertyRecord = new PropertyRecord(propertyId);
				propertyRecord.setInUse(true);
				propertyRecord.setCreated();
				propertyRecord.setKeyIndexId(keyId);
				propStore.encodeValue(propertyRecord, entry.Value);
				if (prevRecord != null)
				{
					prevRecord.setPrevProp(propertyId);
					propertyRecord.setNextProp(prevRecord.getId());
				}
				propRecords.Add(propertyRecord);
				prevRecord = propertyRecord;
			}
		// reverse order results in forward update to store
			for (int i = propRecords.Count - 1; i >=0; i--)
			{
				propStore.updateRecord(propRecords[i]);
			}
			if (prevRecord != null)
			{
				return prevRecord.getId();
			}
			return Record.NO_NEXT_PROPERTY.intValue();
		}

		private void deletePropertyChain(int propertyId)
		{
			PropertyStore propStore = getPropertyStore();
			PropertyRecord propertyRecord = propStore.getRecord(propertyId);
			propertyRecord.setInUse(false);
			foreach (DynamicRecord record in propertyRecord.getValueRecords())
			{
				record.setInUse(false);
			}
			propStore.updateRecord(propertyRecord);
		}

		private Map<string, object> getPropertyChain(int propertyId)
		{
			PropertyStore propStore = getPropertyStore();
			PropertyRecord propertyRecord = propStore.getRecord(propertyId);
			int nextProperty = -1;
			Map<string, object> properties = new Dictionary<string, object>();
			do
			{
				nextProperty = propertyRecord.getNextProp();
				propStore.makeHeavy(propertyRecord);
				string key = indexHolder.getStringKey(propertyRecord.getKeyIndexId());
				object @value = propStore.getValue(propertyRecord);
				properties.put(key, @value);
				if (nextProperty != Record.NO_NEXT_PROPERTY.intValue())
				{
					propertyRecord = propStore.getRecord(propertyRecord.getNextProp());
				}
			} while (nextProperty != Record.NO_NEXT_PROPERTY.intValue());
			return properties;
		}

		private int createNewPropertyIndex(string stringKey)
		{
			PropertyIndexStore idxStore = getPropertyIndexStore();
			int keyId = idxStore.nextId();
			PropertyIndexRecord record = new PropertyIndexRecord(keyId);
			record.setInUse(true);
			record.setCreated();
			int keyBlockId = idxStore.nextKeyBlockId();
			record.setKeyBlockId(keyBlockId);
			int length = stringKey.Length;
			char[] chars = new char[length];
			stringKey.getChars(0, length, chars, 0);
			Collection<DynamicRecord> keyRecords = idxStore.allocateKeyRecords(keyBlockId, chars);
			foreach (DynamicRecord keyRecord in keyRecords)
			{
				record.addKeyRecord(keyRecord);
			}
			idxStore.updateRecord(record);
			indexHolder.addPropertyIndex(stringKey, keyId);
			return keyId;
		}

		private int createNewRelationshipType(string name)
		{
			RelationshipTypeStore typeStore = getRelationshipTypeStore();
			int id = typeStore.nextId();
			RelationshipTypeRecord record = new RelationshipTypeRecord(id);
			record.setInUse(true);
			record.setCreated();
			int typeBlockId = typeStore.nextBlockId();
			record.setTypeBlock(typeBlockId);
			int length = name.Length;
			char[] chars = new char[length];
			name.getChars(0, length, chars, 0);
			Collection<DynamicRecord> typeRecords = typeStore.allocateTypeNameRecords(typeBlockId, chars);
			foreach (DynamicRecord typeRecord in typeRecords)
			{
				record.addTypeRecord(typeRecord);
			}
			typeStore.updateRecord(record);
			typeHolder.addRelationshipType(name, id);
			return id;
		}

		private NodeStore getNodeStore()
		{
			return neoStore.getNodeStore();
		}

		private PropertyStore getPropertyStore()
		{
			return neoStore.getPropertyStore();
		}

		private PropertyIndexStore getPropertyIndexStore()
		{
			return getPropertyStore().getIndexStore();
		}

		private RelationshipStore getRelationshipStore()
		{
			return neoStore.getRelationshipStore();
		}

		private RelationshipTypeStore getRelationshipTypeStore()
		{
			return neoStore.getRelationshipTypeStore();
		}

		private NodeRecord getNodeRecord(long id)
		{
			if (id < 0 || id >= getNodeStore().getHighId())
			{
				throw new NotFoundException("id=" + id);
			}
			return getNodeStore().getRecord((int)(id & 0xFFFFFFFF));
		}

		private RelationshipRecord getRelationshipRecord(long id)
		{
			if (id < 0 || id >= getRelationshipStore().getHighId())
			{
				throw new NotFoundException("id=" + id);
			}
			return getRelationshipStore().getRecord((int)(id & 0xFFFFFFFF));
		}

		private string fixPath(string dir)
		{
			File directories = new File(dir);
			if (!directories.exists())
			{
				if (!directories.mkdirs())
				{
					throw new UnderlyingStorageException("Unable to create directory path[" + storeDir + "] for Neo4j kernel store.");
				}
			}
			dir = FileUtils.fixSeparatorsInPath(dir);
			string fileSeparator = System.getProperty("file.separator");
			string store = dir + fileSeparator + "neostore";
			if (!new File(store).exists())
			{
				NeoStore.createStore(store);
			}
			return store;
		}

		public virtual string getStore()
		{
			return storeDir;
		}

		public static Map<string, string> loadProperties(string file)
		{
			return EmbeddedGraphDatabase.loadConfigurations(file);
		}

		public virtual long getReferenceNode()
		{
			if (nodeExists(0))
			{
				return 0;
			}
			return -1;
		}

		public virtual GraphDatabaseService getGraphDbService()
		{
			return graphDbService;
		}
	}

}